﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.Resources
{
    #region Using Statements

    using System;
    using System.IO;
    using System.Net;
    using System.Linq;
    using KLF.Lib.Browser.DOM;
    using KLF.Lib.Browser.DOM.HTML;
    using KLF.Lib.Browser.DOM.Parser;
    using KLF.Lib.Browser.DOM.Interface;
    using System.Web;

    #endregion

    /// <summary>
    /// This helper class is responsible for completely loading a document
    /// and any subsequent child documents contained within frames, etc.
    /// </summary>
    public class DocumentLoader
    {
        #region Constructors

        public DocumentLoader(RequestContext context)
        {
            Context = context;
        }

        #endregion

        #region Properties

        private RequestContext Context { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Loads and returns a document type.
        /// </summary>
        /// <returns></returns>
        public Document Load()
        {
            Document result = null;

            try
            {
                WebRequest request = Context.BuildRequest();
                WebResponse response = request.GetResponse();
                StreamReader reader = new StreamReader(response.GetResponseStream());
                string responseSource = reader.ReadToEnd();
                result = DocumentBuilder.Parse(responseSource);
                result.Context = Context;
                result.documentURI = Context.Uri.AbsoluteUri;

                if (response is HttpWebResponse)
                {
                    LogHeaders(Context, request as HttpWebRequest, response as HttpWebResponse);
                }

                if (Context.Window.Location != null && !Context.FromHistory)
                {
                    Context.Window.History.HistoryList.Add(Context.Uri);
                }
                LoadResources(Context, result);
                if (Context.EnableJavascript)
                {
                    HTMLDocument doc = result as HTMLDocument;

                    using (JavascriptContext jsc = new JavascriptContext(Context, doc))
                    {
                        var ScriptBlock = (HTMLScriptElement)result
                            .Elements
                            .DefaultIfEmpty(null)
                            .FirstOrDefault(e =>
                            {
                                HTMLScriptElement s = e as HTMLScriptElement;

                                return s != null && !s.HasExecuted;
                            });

                        while (ScriptBlock != null)
                        {
                            try
                            {
                                ScriptBlock.HasExecuted = true;

                                //  If the code block is inline then take the code from the text.
                                if (String.IsNullOrEmpty(ScriptBlock.Code))
                                    ScriptBlock.Code = ScriptBlock.text;

                                if (Context.Browser.ScriptPreExecute(doc, ScriptBlock))
                                {
                                    ScriptBlock.HasExecuted = true;
                                    jsc.ExecuteScript(ScriptBlock);
                                    Context.Browser.ScriptPostExecute(doc, ScriptBlock, true);
                                }
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("DocumentLoader exception: {0}", ex.Message);
                            }

                            //  Once a code block is executed, null the code block out to release memory.
                            ScriptBlock.Code = String.Empty;

                            //  Check for additional resources to load.
                            LoadResources(Context, result);

                            //  Get next unexecuted script block.
                            ScriptBlock = (HTMLScriptElement)result
                                .Elements
                                .DefaultIfEmpty(null)
                                .FirstOrDefault(e =>
                                {
                                    HTMLScriptElement s = e as HTMLScriptElement;

                                    return s != null && !s.HasExecuted;
                                });
                        }
                    }
                }
                result._state = DocumentState.Interactive;
            }
            catch (Exception ex)
            {
                Context.Browser.Error(Context, ex);
                throw ex;
            }

            // TODO: Optionally load other resources... and then...
            result.normalizeDocument();
            result._state = DocumentState.Complete;

            return result;
        }

        private void LogHeaders(RequestContext context, HttpWebRequest request, HttpWebResponse response)
        {
            Console.WriteLine("---------------------------------------------------------------------------");
            Console.WriteLine(request.RequestUri);
            Console.WriteLine("{0} {1}", 
                context.Verb.ToUpperInvariant(), 
                request.RequestUri.PathAndQuery);
            foreach (var item in request.Headers.AllKeys)
            {
                Console.WriteLine("{0}: {1}", item, request.Headers[item]);
            }
            var cookieString = request.CookieContainer.GetCookieHeader(request.RequestUri);
            if (cookieString.Length > 0)
                Console.WriteLine("Cookie: {0}", cookieString);
            if (!String.IsNullOrEmpty(context.PostData))
                Console.WriteLine("\t{0}", context.PostData);

            Console.WriteLine();

            Console.WriteLine("Response: HTTP/{0} {1} {2}",
                response.ProtocolVersion,
                ((int)response.StatusCode),
                response.StatusDescription);
            Console.WriteLine(response.ResponseUri.ToString());
            foreach (var item in response.Headers.AllKeys)
            {
                Console.WriteLine("{0}: {1}", item, response.Headers[item]);
            }

            Console.WriteLine("---------------------------------------------------------------------------");
        }

        /// <summary>
        /// Loads CSS files, frames, script files.
        /// </summary>
        /// <param name="context"></param>
        /// <param name="ActiveDocument"></param>
        private void LoadResources(RequestContext context, Document ActiveDocument)
        {
            ResourceLoader loader = new ResourceLoader(context);
            var items = ActiveDocument.Elements.Where(e =>
            {
                //  Prevent content from loading multiple times.
                ILoadableContent foo = e as ILoadableContent;

                if (foo != null && foo.HasLoaded)
                    return false;

                if (e.TagName.Equals(HTMLConstants.HTML_TAG_LINK) &&
                    e.getAttribute(HTMLConstants.HTML_ATT_REL)
                    .Equals(HTMLConstants.HTML_REL_STYLESHEET)) return true;

                if (e.TagName.Equals(HTMLConstants.HTML_TAG_SCRIPT) &&
                    !String.IsNullOrEmpty(e.getAttribute(HTMLConstants.HTML_ATT_SRC))) return true;

                if (e.TagName.Equals(HTMLConstants.HTML_TAG_FRAME) &&
                    !String.IsNullOrEmpty(e.getAttribute(HTMLConstants.HTML_ATT_SRC))) return true;

                if (e.TagName.Equals(HTMLConstants.HTML_TAG_IFRAME) &&
                    !String.IsNullOrEmpty(e.getAttribute(HTMLConstants.HTML_ATT_SRC))) return true;

                return false;
            })
            .Cast<ILoadableContent>();

            foreach (ILoadableContent item in items)
            {
                if (item is HTMLLinkElement)
                {
                    if (item.getAttribute(HTMLConstants.HTML_ATT_REL)
                        .Equals(HTMLConstants.HTML_REL_STYLESHEET))
                    {
                        if (Context.Browser.Options.EnableCss)
                            loader.AddResourceRequest(
                                new StyleRequest(item.getAttribute(HTMLConstants.HTML_ATT_HREF), item));
                        else
                            item.HasLoaded = true;
                    }
                }
                else if (item is HTMLStyleElement)
                {
                    if (Context.Browser.Options.EnableCss)
                        loader.AddResourceRequest(
                            new StyleRequest(item.getAttribute(HTMLConstants.HTML_ATT_HREF), item));
                    else
                        item.HasLoaded = true;
                }
                else if (item is HTMLScriptElement)
                {
                    if (Context.Browser.Options.EnableJavascript)
                        loader.AddResourceRequest(
                            new ScriptRequest(item.getAttribute(HTMLConstants.HTML_ATT_SRC), item));
                    else
                        item.HasLoaded = true;
                }
                else if (item is HTMLFrameElement)
                {
                    loader.AddResourceRequest(
                        new FrameRequest(item.getAttribute(HTMLConstants.HTML_ATT_SRC), item));
                }
                else if (item is HTMLIFrameElement)
                {
                    loader.AddResourceRequest(
                        new IFrameRequest(item.getAttribute(HTMLConstants.HTML_ATT_SRC), item));
                }
            }
            loader.Execute();
        }

        #endregion
    }
}
